﻿
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Okarito.Utility;

namespace Okarito
{
    public sealed class FogBugzClient
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="FogBugzClient"/> class.
        /// </summary>
        /// <param name="server">The FogBugz server.</param>
        public FogBugzClient(Uri server)
        {
            if (server == null)
                throw new ArgumentNullException("server");

            m_urlPrefix = GetApiUrl(server);
            if (m_urlPrefix == null)
                throw new ArgumentException("The specified server is not a valid FogBugz server, or the API version is not supported.", "server");
        }

        /// <summary>
        /// Logs on. This method must be called before any communications with the server.
        /// </summary>
        /// <param name="userName">The FogBugz user name.</param>
        /// <param name="password">The password.</param>
        /// <returns>A <see cref="Scope"/> that can be disposed to log off.</returns>
        public Scope LogOn(string userName, string password)
        {
            if (IsLoggedOn)
                throw new InvalidOperationException("Already logged on; call LogOff first to change credentials.");

            XElement response = SendRequest("logon", "email", userName, "password", password).AsSynchronous();
            m_token = (string) response.Element("token");
            return Scope.Create(LogOff);
        }

        /// <summary>
        /// Logs off. Communications with the server will fail after this method is called, unless <see cref="LogOn"/> is called again.
        /// </summary>
        /// <remarks>If the client is not currently logged on, this method does nothing.</remarks>
        public void LogOff()
        {
            if (IsLoggedOn)
            {
                SendRequest("logoff");
                m_token = null;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this client is logged on.
        /// </summary>
        /// <value><c>true</c> if this client is logged on; otherwise, <c>false</c>.</value>
        public bool IsLoggedOn
        {
            get { return m_token != null; }
        }

        /// <summary>
        /// Asynchronously gets the filters available to the logged-on user.
        /// </summary>
        /// <returns>A Task that returns a collection of <see cref="Filter"/> objects representing the filters available to the current user when complete.</returns>
        public Task<ReadOnlyCollection<Filter>> GetFilters()
        {
            // get the work intervals
            return SendRequest("listFilters").
                ContinueWith<ReadOnlyCollection<Filter>>(FinishGetFilters);
        }

        // Converts the result of the "listFilters" command into Filter objects.
        private ReadOnlyCollection<Filter> FinishGetFilters(Task<XElement> task)
        {
            // process the response
            List<Filter> filters = new List<Filter>();
            foreach (XElement filter in task.Result.NestedElements("filter"))
            {
                // parse values from the XML
                string id = (string) filter.Attribute("sFilter");
                FilterType type = Filter.ParseType((string) filter.Attribute("type"));
                string title = (string) filter;

                // add a new filter
                filters.Add(new Filter(id, type, title));
            }
            return filters.AsReadOnly();
        }

        /// <summary>
        /// Gets the work intervals for work on the specified day.
        /// </summary>
        /// <param name="day">The day.</param>
        /// <returns>A collection of <see cref="WorkInterval"/> objects for work on that day.</returns>
        /// <remarks>The <see cref="DateTime.Kind"/> property determines whether the <paramref name="day"/> is measured in local or UTC time.</remarks>
        public Task<ReadOnlyCollection<WorkInterval>> GetIntervals(DateTime day)
        {
            return GetIntervals(day.Date, day.Date.AddDays(1));
        }

        /// <summary>
        /// Gets the work intervals for the period delimited by the specified start and end points.
        /// </summary>
        /// <param name="start">The start point.</param>
        /// <param name="end">The end point.</param>
        /// <returns>A collection of <see cref="WorkInterval"/> objects for work in the specified period.</returns>
        public Task<ReadOnlyCollection<WorkInterval>> GetIntervals(DateTime start, DateTime end)
        {
            // get the work intervals
            return SendRequest("listIntervals", "dtStart", start.ToIso8601(), "dtEnd", end.ToIso8601()).
                ContinueWith<ReadOnlyCollection<WorkInterval>>(FinishGetIntervals);
        }

        private ReadOnlyCollection<WorkInterval> FinishGetIntervals(Task<XElement> task)
        {
            // process the response
            List<WorkInterval> listIntervals = new List<WorkInterval>();
            foreach (XElement interval in task.Result.NestedElements("interval"))
            {
                // parse values from the XML
                int bug = (int) interval.Element("ixBug");
                DateTime workStart = DateTimeUtility.ParseIso8601((string) interval.Element("dtStart"));
                string strWorkEnd = (string) interval.Element("dtEnd");
                DateTime workEnd = string.IsNullOrEmpty(strWorkEnd) ? DateTime.UtcNow : DateTimeUtility.ParseIso8601(strWorkEnd);

                // add a new work interval
                listIntervals.Add(new WorkInterval(bug, workStart, workEnd));
            }
            return listIntervals.AsReadOnly();
        }

        // Sends a request to the FogBugz API and returns the results as XML.
        // "command" must be specified; the optional "parameters" array should contain a set of matched key/value pairs.
        private Task<XElement> SendRequest(string command, params string[] parameters)
        {
            // check parameters
            if (command == null)
                throw new ArgumentNullException("command");
            if (parameters == null)
                throw new ArgumentNullException("parameters");
            if (parameters.Length % 2 != 0)
                throw new ArgumentException("Invalid number of parameters", "parameters");

            // build the request URI
            StringBuilder url = new StringBuilder(m_urlPrefix + "cmd=");
            url.Append(Uri.EscapeDataString(command));

            // append authentication token
            if (m_token != null)
                url.AppendFormat("&token={0}", Uri.EscapeDataString(m_token));

            // append each parameter
            for (int nParameter = 0; nParameter < parameters.Length; nParameter += 2)
                url.AppendFormat("&{0}={1}", Uri.EscapeDataString(parameters[nParameter]), Uri.EscapeDataString(parameters[nParameter + 1]));

            // download the response (asynchronously)
            WebRequest request = WebRequest.Create(url.ToString());
            Task<WebResponse> responseTask = Task<WebResponse>.Factory.FromAsync(request.BeginGetResponse, request.EndGetResponse, null);
            return responseTask.ContinueWith<XElement>(OnGetResponse);
        }

        private XElement OnGetResponse(Task<WebResponse> task)
        {
            XDocument docResponse;
            using (WebResponse response = task.Result)
            using (Stream stream = response.GetResponseStream())
            using (XmlReader reader = XmlReader.Create(stream))
                docResponse = XDocument.Load(reader);

            // check response for success
            if (docResponse.Root == null)
                throw new FogBugzException(0, "No response received from FogBugz server.");
            XElement error = docResponse.Root.Element("error");
            if (error != null)
                throw new FogBugzException((int) error.Attribute("code"), (string) error);

            return docResponse.Root;
        }

        // Accesses "api.xml" at the specified URL, parses the FogBugz response, and builds the URL prefix for all subsequent requests.
        private static string GetApiUrl(Uri server)
        {
            // download "api.xml"
            Uri apiXmlUrl = new Uri(server, "api.xml");
            XDocument document = XDocument.Load(apiXmlUrl.ToString());
            if (document == null || document.Root == null)
                return null;

            // check if the right version is supported
            XElement response = document.Root;
            int version = (int) response.Element("version");
            int minVersion = (int) response.Element("minversion");
            bool isVersionSupported = version >= 6 && minVersion <= 6;

            return isVersionSupported ? apiXmlUrl.ToString().Replace("api.xml", (string) response.Element("url")) : null;
        }

        readonly string m_urlPrefix;

        string m_token;
    }
}
